package org.cogaen.sound;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.Mixer;
import javax.sound.sampled.SourceDataLine;
import org.cogaen.core.Core;
import org.cogaen.core.Service;

public class SoundService extends ThreadPool implements Service  {
	
	/** Unique name of this service. */	
	public static final String NAME = "cogaen.soundservice";
	
	/** @brief The audio playback format used to play files. */
	private AudioFormat playbackFormat;
	
	/** @brief A thread local to find playable bytes. */
	private ThreadLocal<SourceDataLine> localLine;

	/** @brief A local thread buffer to prefetch audio lines. */
	private ThreadLocal<byte[]> localBuffer;
	
	/** @brief A mutex to synchronize some critical sections. */
	private Object pausedLock;
	
	/** @brief A flag to indicate if the player is paused. */
	private boolean paused;
	
	/** @ brief The engine's core. */
	private Core core;	
	
	private SoundPlayer backgroundPlayer;
	
	/**
	 * Returns the installed instance of this service.
	 * 
	 * <p>This is a convenience method. A call to this method is identical
	 * to the following code:
	 * <pre>
	 * (LoggingService) core.getService(LoggingService.NAME);
	 * </pre>
	 * </p>
	 * 
	 * @param core reference to a instance of core.
	 * @return installed service instance.
	 */
	public static SoundService getInstance(Core core) {
		return (SoundService) core.getService(NAME);
	}
	
	/**
	 * @brief The default constructor that creates a new thread pool.
	 * 
	 * @param playbackFormat The playback format for the audio files.
	 * @param maxSimultaneousSounds The maximum amount of sounds that can be played at the same time.
	 */
	public SoundService(AudioFormat playbackFormat, int maxSimultaneousSounds) {
		/*
		 *  NOTE: Normally, the Java Sound API is capable of getting the amount
		 *  of sound files that can be played by the computer's hardware at the
		 *  same time dynamically. However, there is a known bug which results
		 *  in a wrong return value in JSAPI 1.5. Therefore, the amount of sounds
		 *  is set to 16 for now.
		 */
		super(NAME, 16);
		//super(NAME, maxSimultaneousSounds);
		
		this.playbackFormat = playbackFormat;
		this.localLine = new ThreadLocal<SourceDataLine>();
		this.localBuffer = new ThreadLocal<byte[]>();
		this.pausedLock = new Object();
		
		synchronized (this) {
			notifyAll();
		}
	}
	
	/**
	 * @brief Default constructor for wav playback.
	 * 
	 * The wav files have fulfill the following criterias:
	 * 
	 * Encoding: PCM_SIGNED
	 * Samplerate: 44.1 kHz
	 * Sample bit rate: 16
	 * Channels: 2 (Stereo)
	 * Framesize: 44.1 kHz
	 * Frame bit rate: 4
	 * Format: Little endian
	 * 
	 * @param playbackFormat The playback format to interpret the audio files.
	 */
	public SoundService() {
		this(new AudioFormat(AudioFormat.Encoding.PCM_SIGNED, 44100f, 16, 2, 4, 44100f, false), 
				getMaxSimultaneousSounds(new AudioFormat(AudioFormat.Encoding.PCM_SIGNED, 44100f, 16, 2, 4, 44100f, false)));
	}
	
	/**
	 * @brief Custom constructor where only the playback format is provided.
	 * 
	 * @param playbackFormat The playback format to interpret the audio files.
	 */
	public SoundService(AudioFormat playbackFormat) {
		this(playbackFormat, getMaxSimultaneousSounds(playbackFormat));
	}

	/**
	 * @brief Initializes the service with the engine's core.
	 * 
	 * @param core The engine's core.
	 */
	@Override
	public void initialize(Core core) {
		this.core = core;
	}

	/**
	 * @brief Immediately stops the hardware playing any sounds.
	 */
	protected void cleanUp() {
		setPaused(false);
		Mixer mixer = AudioSystem.getMixer(null);
		if (mixer.isOpen())
			mixer.close();
	}

	/**
	 * @brief Immediately stops the hardware playing any sounds and closes the thread pool.
	 */
	@Override
	public void close() {
		cleanUp();
		super.close();
	}
	
	/**
	 * @brief Immediately stops the hardware playing any sounds and joins the thread pool.
	 */
	@Override
	public void join() {
		cleanUp();
		super.join();
	}

	/**
	 * @brief Sets the player paused or playing using a mutex to avoid non-synchronous access.
	 * 
	 * @param paused
	 */
	public void setPaused(boolean paused) {
		if (this.paused != paused) {
			synchronized (pausedLock) {
				this.paused = paused;
				if (!paused)
					pausedLock.notifyAll();
			}
		}
	}

	/**
	 * @brief Checks the player's state.
	 * 
	 * @return true if the player is paused; otherwise false.
	 */
	public boolean isPaused() {
		return paused;
	}

	/**
	 * @brief Evaluates how many audio files from a certain format can be played at the same time.
	 * 
	 * @param playbackFormat The format to be used for playing files.
	 * @return The number of supported samples at the same time.
	 */
	public static int getMaxSimultaneousSounds(AudioFormat playbackFormat) {
		DataLine.Info lineInfo = new DataLine.Info(SourceDataLine.class, playbackFormat);
		
		// NOTE: In version 1.5 of Java Sound API this always returns 0 and cannot be used.
		Mixer mixer = AudioSystem.getMixer(null);
		return mixer.getMaxLines(lineInfo);
	}

	/**
	 * Plays a sound effect.
	 * 
	 * @param sound The sound effect to be played.
	 * @return An input stream to be played.
	 */
	public InputStream play(SoundEffect sound) {
		return play(sound, null);
	}
	
	/**
	 * Plays looped background music until it is stopped again.
	 * 
	 * @param music The music to be played.
	 */
	public void playBackgroundMusic(SoundEffect music) {
		if (music != null) {
			if(backgroundPlayer != null) {
				backgroundPlayer.stopThread();
				backgroundPlayer = null;
			}
			
			InputStream is = new LoopingByteInputStream((byte[])music.getStream());
			backgroundPlayer = new SoundPlayer(is);
			runTask(backgroundPlayer);
		}
	}
	
	/**
	 * Stops currently playing background music.
	 */
	public void stopBackgroundMusic() {
		if(backgroundPlayer != null) {
			backgroundPlayer.stopThread();
			stopTask(backgroundPlayer);
		}
	}

	/**
	 * Plays an input stream.
	 * 
	 * @param sound The sound effect to be played.
	 * @param filter The filter to be applied.
	 * 
	 * @return The input stream to be played.
	 */
	public InputStream play(SoundEffect sound, SoundFilter filter) {
		if (sound != null) {
			InputStream is = new ByteArrayInputStream((byte[])sound.getStream());
			return play(is, filter);
		}
		return null;
	}

	/**
	 * @brief Plays an input stream.
	 * 
	 * @param is The input stream to be played.
	 * @return The played input stream.
	 */
	public InputStream play(InputStream is) {
		return play(is, null);
	}

	/**
	 * @brief Plays a filtered input stream.
	 * 
	 * @param is The input stream to be played.
	 * @param filter The filter to be applied.
	 * @return The played input stream.
	 */
	public InputStream play(InputStream is, SoundFilter filter) {
		if (is != null) {
			if (filter != null) {
				is = new FilteredSoundStream(is, filter);
			}
			runTask(new SoundPlayer(is));
		}
		return is;
	}

	/**
	 * @brief Create an audio line to be played and refill the local buffer.
	 */
	protected void threadStarted() {
		synchronized (this) {
			try {
				wait();
			} catch (InterruptedException ex) {
				// TODO: handle exception
			}
		}
		
		int buffersize = playbackFormat.getFrameSize() * Math.round(playbackFormat.getSampleRate() / 10);
		SourceDataLine line;
		DataLine.Info lineInfo = new DataLine.Info(SourceDataLine.class,
				playbackFormat);
		try {
			line = (SourceDataLine) AudioSystem.getLine(lineInfo);
			line.open(playbackFormat, buffersize);
		} catch (LineUnavailableException ex) {
			Thread.currentThread().interrupt();
			return;
		}
		line.start();
		byte[] buffer = new byte[buffersize];

		localLine.set(line);
		localBuffer.set(buffer);
	}

	/**
	 * @brief Erase an open line and close it.
	 */
	protected void threadStopped() {
		SourceDataLine line = localLine.get();
		if (line != null) {
			line.drain();
			line.close();
		}
	}

	/**
	 * @class SoundPlayer
	 * @author Florian Lettner
	 * 
	 * @brief Represents a player that can play input streams.
	 */
	protected class SoundPlayer implements Runnable {
		/** @brief A common or loopable input stream. */
		private InputStream source;
		
		private volatile boolean stop = false;

		/**
		 * @brief Creates a new sound player instance.
		 * 
		 * @param source The input stream to be played.
		 */
		public SoundPlayer(InputStream source) {
			this.source = source;
		}

		/**
		 * @brief Runs the provided input stream.
		 */
		@Override
		public void run() {
			SourceDataLine line = localLine.get();
			byte[] buffer = localBuffer.get();
			
			if (line == null || buffer == null)
				return;
			
			try {
				int numBytesRead = 0;
				
				while (numBytesRead != -1 && !stop) {
					synchronized (pausedLock) {
						if (paused) {
							try {
								pausedLock.wait();
							} catch (InterruptedException ex) {
								return;
							}
						}
					}
					numBytesRead = source.read(buffer, 0, buffer.length);
					if (numBytesRead != -1)
						line.write(buffer, 0, numBytesRead);
				}
			} catch (IOException ex) {
				ex.printStackTrace();
			}
		}
		
		/**
		 * Causes the thread to stop asynchronously.
		 */
		public void stopThread() {
			stop = true;
		}
	}	
}
